package qj.tool.gae.db;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import qj.util.Cols;
import qj.util.ReflectUtil;
import qj.util.ZipUtil;
import qj.util.funct.F1;
import qj.util.funct.P2;

import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Text;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class AEDB {

	public static interface GaeDataConverter<A> {
		boolean isNamedId();
		boolean isId();
		Key getKey(A obj);
		P2<Object,Entity> ote();
		A eto(Entity e);
		String getNameId(A obj);
		Long getId(A obj);
		void setId(Object id, Object obj);
		HashMap<String,Field> fields();
	}
	
	public static <A> GaeDataConverter<A> analyze(final A a) {
		Class class1 = a.getClass();
		return (GaeDataConverter<A>) analyze(class1);
	}

	public static <A> GaeDataConverter<A> analyze(final Class<A> clazz) {
		
		final HashMap<String,Field> fields = new HashMap<String,Field>();
		final List<P2<Object, Entity>> oteTasks = new LinkedList<P2<Object, Entity>>();
		final List<P2<Entity, Object>> etoTasks = new LinkedList<P2<Entity, Object>>();
		
		analyze(clazz, fields, oteTasks, etoTasks);

		final Field idField = fields.get("id");
		
		return new GaeDataConverter<A>() {
			public P2<Object, Entity> ote() {
				return new P2<Object, Entity>() {public void e(Object o, Entity e) {
					for (P2<Object,Entity> p2 : oteTasks) {
						p2.e(o, e);
					}
				}};
			}
			public A eto(Entity e) {
				A o = ReflectUtil.newInstance(clazz);

				for (P2<Entity,Object> p2 : etoTasks) {
					p2.e(e, o);
				}
				return o;
			}

			public boolean isNamedId() {
				return idField==null ? false : idField.getType().equals(String.class);
			}
			public boolean isId() {
				return idField==null ? true : idField.getType().equals(Long.class);
			}
			public String getNameId(A obj) {
				return (String) getFieldValue(idField, obj);
			}
			public Long getId(A obj) {
				return idField == null ? null : (Long) getFieldValue(idField, obj);
			}
			public Key getKey(A obj) {
				return (Key) getFieldValue(idField, obj);
			}
			@Override
			public void setId(Object id, Object obj) {
				if (idField==null) {
					return;
				}
				
				ReflectUtil.setFieldValue(id, idField, obj);
			}
			public HashMap<String,Field> fields() {
				return fields;
			}
		};
	}
	

	public static void analyze(Class<?> clazz, HashMap<String, Field> fields,
			List<P2<Object, Entity>> oteTasks,
			List<P2<Entity, Object>> etoTasks) {
		for (final Field field : clazz.getDeclaredFields()) {
			int modifiers = field.getModifiers();
			if ((modifiers & (Modifier.STATIC | Modifier.FINAL | Modifier.TRANSIENT)) > 0
					|| (modifiers & (Modifier.PUBLIC)) == 0
					) {
				continue;
			}
			
			String name = field.getName();
	
			if (fields.containsKey(name)) {
				continue;
			}
			fields.put(name,field);
	
			oteTasks.add(ote(field));
			
			etoTasks.add(eto(field));
		}
		
		if (clazz.getSuperclass() != Object.class) {
			analyze(clazz.getSuperclass(), fields, oteTasks, etoTasks);
		}
	}

	public static P2<Entity, Object> eto(final Field field) {
		final Unindexed unindexedAnno = field.getAnnotation(Unindexed.class);
		
		return new P2<Entity, Object>() {public void e(Entity e, Object o) {
			final String name = field.getName();
			Class<?> type = field.getType();
			
			if (name.equals("id")) {
				Object value = 
						type.equals(String.class) ? e.getKey().getName() : 
						type.equals(Long.class) ? e.getKey().getId() : 
						e.getKey()
						;
				ReflectUtil.setFieldValue(value, field, o);

			} else if (Map.class.isAssignableFrom(type)) {
				ReflectUtil.setFieldValue(getMap(name, e), field, o);
			} else if (List.class.isAssignableFrom(type)) {
				ReflectUtil.setFieldValue(getList(name, e), field, o);
			} else if (type.isPrimitive() && e.getProperty(name)==null) {
				;
			} else {
				Object value = e.getProperty(name);
				
				if (type.equals(String.class)) {
					if (unindexedAnno != null && unindexedAnno.zip()) {
						value = ZipUtil.gunzipString(((Blob)value).getBytes());
					} else if (value instanceof Text) {
						value = ((Text)value).getValue();
					}
				} else if (value instanceof String && type.equals(Text.class)) {
					value = new Text(name);
				} else if (type.equals(byte[].class)) {
					if (value==null) {
						// Let it be
					} else if (unindexedAnno != null && unindexedAnno.zip()) {
						value = ZipUtil.gunzip(((Blob)value).getBytes());
					} else {
						value = ((Blob)value).getBytes();
					}
				} else if (value instanceof Long && (type.equals(Integer.class) || type.equals(int.class))) {
					value = ((Long)value).intValue();
				}
				ReflectUtil.setFieldValue(value, field, o);
			}
		}};
	}
	
	public static void main(String[] args) {
		LinkedHashMap<String, String> hashMap = new LinkedHashMap<String, String>();
		hashMap.put(null, "aa");

		for (Entry<String, String> entry : hashMap.entrySet()) {
			String key = entry.getKey();
			System.out.println(key);
//			if (key==null) {
//				
//			} else {
//				setter.e(name + "Key" + i, key);
//				setter.e(name + "Value" + i, entry.getValue());
//				i++;
//			}
		}
//		System.out.println(hashMap.get(null));
	}
	

	protected static Map<String, String> getMap(String name, Entity e) {
		LinkedHashMap<String, String> map = new LinkedHashMap<String,String>();
		
		// Get default value
		String defValue = getString(name,e);
		if (defValue != null) {
			map.put(null, defValue);
		}
		
		for (int i = 0; ; i++) {
			String key = getString(name + "Key" + i, e);
			if (key == null) {
				break;
			}
			String value = getString(name + "Value" + i, e);
			map.put(key, value);
		}
		return map;
	}
	private static String getString(String name, Entity e) {
		Object val = e.getProperty(name);
		if (val == null) {
			return null;
		} else if (val instanceof String) {
			return (String) val;
		} else if (val instanceof Text) {
			return ((Text) val).getValue();
		} else {
			return null;
		}
	}


	protected static List getList(String name, Entity e) {
		LinkedList list = new LinkedList();
		for (int i = 0; ; i++) {
			Object value = e.getProperty(name + i);
			if (value==null) {
				break;
			}
			list.add(value);
		}
		return list;
	}

	public static P2<Object, Entity> ote(final Field field) {
		final Unindexed unindexedAnno = field.getAnnotation(Unindexed.class);
		final String name = field.getName();
		P2<Object, Entity> ote = new P2<Object, Entity>() {public void e(Object o, final Entity e) {
			Object value = getFieldValue(field, o);
			
			if (value==null) {
				return;
			}
			
			P2<String, Object> setter = unindexedAnno != null ? unindexedValueSetter(e, unindexedAnno) : indexedValueSetter(e);
			if (value instanceof Map) {
				setMap((Map<String, String>) value, name, setter);
			} else if (value instanceof List) {
				setList((List) value, name, setter);
			} else if (unindexedAnno != null) {
				e.setUnindexedProperty(name, unindexValue(value, unindexedAnno));
			} else {
				if (!name.equals("id")) {
					e.setProperty(name, value);
				}
			}
		}};
		return ote;
	}	
	
	protected static void setMap(Map<String,String> value, String name, P2<String, Object> setter) {
		int i = 0;
		for (Entry<String, String> entry : value.entrySet()) {
			String key = entry.getKey();
			if (key==null) {
				setter.e(name, entry.getValue());
			} else {
				setter.e(name + "Key" + i, key);
				setter.e(name + "Value" + i, entry.getValue());
				i++;
			}
		}
	}
	protected static void setList(List list, String name, P2<String, Object> setter) {
		int i = 0;
		for (Object val : list) {
			setter.e(name + i, val);
			i++;
		}
	}


	protected static Object unindexValue(Object value, Unindexed unindexedAnno) {
		if (value instanceof String) {
			String strValue = (String) value;
			if (unindexedAnno.zip()) {
				return new Blob(ZipUtil.gzip(strValue));
			} else if (strValue.length() < 500) {
				return strValue;
			} else {
				return new Text(strValue);
			}
		} else if (value instanceof byte[]) {
			if (unindexedAnno.zip()) {
				return new Blob(ZipUtil.gzip((byte[]) value));
			} else {
				return new Blob((byte[]) value);
			}
		} else {
			return value;
		}
	}


	public static Object getFieldValue(final Field field, Object obj) {
		if (obj==null) {
			return null;
		}
		return ReflectUtil.getFieldValue(field, obj);
	}
	

	public static Key changeParentKey(Key fromParent, Key toParent, Key key, LinkedList<KeyPart> childs) {
		if (key.equals(fromParent) ) {
			return build(toParent, childs);
		}
		childs.add(0, KeyPart.get(key));
		
		Key parent = key.getParent();
		if (parent==null) {
			if (fromParent!=null) {
				throw new RuntimeException("No parent: " + key);
			} else {
				return build(toParent, childs);
			}
		}
		
		Key move = changeParentKey(fromParent, toParent, parent, childs);
		if (move!=null) {
			return move;
		} else if (childs.isEmpty()) {
			throw new RuntimeException("Can not find parent " + fromParent + " of key " + key);
		} else {
			throw new RuntimeException("Can determine move parent key: fromParent=" + fromParent + ", toParent=" + toParent + ", key=" + key);
		}
		
	}
	
	public static List<KeyPart> detach(Key key, LinkedList<KeyPart> parts) {
		parts.addFirst(KeyPart.get(key));
		Key parent = key.getParent();
		if (parent==null) {
			return parts;
		} else {
			return detach(parent, parts);
		}
	}
	
	public static Key build(Key parent, LinkedList<KeyPart> childs) {
		if (childs.isEmpty()) {
			return parent;
		}
		Key key = childs.pop().toKey(parent);
		return build(key, childs);
	}

	public static class KeyPart {
		public String kind;
		public Long id;
		public String name;
		public KeyPart(String kind, Long id) {
			this.kind = kind;
			this.id = id;
		}
		public KeyPart(String kind, String name) {
			this.kind = kind;
			this.name = name;
		}
		public static KeyPart get(Key key) {
			String keyName = key.getName();
			if (keyName==null) {
				return new KeyPart(key.getKind(), key.getId());
			} else {
				return new KeyPart(key.getKind(), keyName);
			}
		}
		public Key toKey(Key parent) {
			if (id!=null) {
				return KeyFactory.createKey(parent, kind, id);
			} else {
				return KeyFactory.createKey(parent, kind, name);
			}
		}
	}

	public static Key key(Key parent, String kind, LinkedList<String> list) {
		String user = list.pop();
		Key thisKey = KeyFactory.createKey(parent, kind, user);
		if (Cols.isNotEmpty(list)) {
			return key(thisKey, kind, list);
		} else {
			return thisKey;
		}
	}


	private static P2<String, Object> indexedValueSetter(final Entity e) {
		return new P2<String, Object>() {public void e(String name, Object value) {
			e.setProperty(name, value);
		}};
	}

	private static P2<String, Object> unindexedValueSetter(final Entity e, final Unindexed unindexedAnno) {
		return new P2<String, Object>() {public void e(String name, Object value) {
			Object val = unindexValue(value, unindexedAnno);
			e.setUnindexedProperty(name, val);
		}};
	}

	public static F1<Key,String> keyToStringF = new F1<Key,String>() {public String e(Key key) {
		return key==null ? null : key.toString();
	}};
	public static F1<String, Key> stringToKeyF = new F1<String,Key>() {public Key e(String str) {
		return stringToKey(str);
	}};
	public static Key stringToKey(String str) {
		if (str== null) {
			return null;
		}
		LinkedList<KeyPart> keyParts = toKeyParts(str);
		return build(null, keyParts);
	}

	public static Key stringAsciiToKey(String str) {
		return stringToKey(decodeAsciiKeyString(str));
	}


	public static String decodeAsciiKeyString(String str) {
		return str
				.replaceAll("W", "\"")
				.replaceAll("C", "(")
				.replaceAll("V", ")")
				.replaceAll("X", "/");
	}
	public static String asciiKeyString(String str) {
		return str
				.replaceAll("\"","W")
				.replaceAll("\\(","C")
				.replaceAll("\\)","V")
				.replaceAll("/","X")
				;
	}


	static Pattern keyPartPtn = Pattern.compile("(\\w+)\\((.+)\\)");
	private static LinkedList<KeyPart> toKeyParts(String str) {
		LinkedList<KeyPart> list = new LinkedList<KeyPart>();
		for (String s : str.split("/")) {
			list.add(toKeyPart(s));
		}
		return list;
	}


	private static KeyPart toKeyPart(String s) {
		Matcher matcher = keyPartPtn.matcher(s);
		if (!matcher.matches()) {
			throw new RuntimeException(s);
		}
		String type = matcher.group(1);
		String value = matcher.group(2);
		if (value.startsWith("\"")) {
			return new KeyPart(type, value.substring(1,value.length() - 1));
		} else {
			return new KeyPart(type, Long.valueOf(value));
		}
	}
	

}
